/*
 * $Id: LocalHttpServer.java 629 2008-09-24 02:08:52Z jasta00 $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package org.devtcg.five.music.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;

import org.apache.http.HttpServerConnection;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;

import android.util.Log;

public abstract class LocalHttpServer extends Thread
{
	public static final String TAG = "LocalHttpServerHack";
	
	protected final HashSet<WorkerThread> mWorkers =
	  new HashSet<WorkerThread>();

	protected ServerSocket mSocket;
	protected HttpParams mParams;
	private HttpRequestHandler mReqHandler;

	public LocalHttpServer(int port)
	  throws IOException
	{
		mSocket = new ServerSocket();
		mSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));

		mParams = new BasicHttpParams();
		mParams
		  .setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
		  .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
		  .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK,
		    false)
		  .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true);
	}
	
	public void setRequestHandler(HttpRequestHandler handler)
	{
		mReqHandler = handler;
	}

	public int getPort()
	{
		return mSocket.getLocalPort();
	}

	public void shutdown()
	{
		synchronized(mWorkers) {
			for (WorkerThread t: mWorkers)
			{
				t.shutdown();
				t.joinUninterruptibly();
			}

			mWorkers.clear();
		}
		
		interrupt();

		try {
			mSocket.close();
		} catch (IOException e) {}
	}

	public void run()
	{
		assert mReqHandler != null;

		while (Thread.interrupted() == false)
		{
			try {
				Socket sock = mSocket.accept();
				DefaultHttpServerConnection conn =
				  new DefaultHttpServerConnection();
				
				Log.i(TAG, "Incoming connection from " +
				  sock.getInetAddress());

				conn.bind(sock, mParams);

				BasicHttpProcessor proc = new BasicHttpProcessor();
				proc.addInterceptor(new ResponseContent());
				proc.addInterceptor(new ResponseConnControl());

				HttpRequestHandlerRegistry reg =
				  new HttpRequestHandlerRegistry();				
				reg.register("*", mReqHandler);

				HttpService svc = new HttpService(proc,
				  new DefaultConnectionReuseStrategy(),
				  new DefaultHttpResponseFactory());

				svc.setParams(mParams);
				svc.setHandlerResolver(reg);

				WorkerThread t;

				synchronized(mWorkers) {
					t = new WorkerThread(svc, conn);					
					mWorkers.add(t);
				}

				t.setDaemon(true);
				t.start();
			} catch (IOException e) {
				Log.e(TAG, "I/O error initializing connection thread: " +
				  e.getMessage());
				break;
			}
		}
	}

	private class WorkerThread extends Thread
	{
		private HttpService mService;
		private HttpServerConnection mConn;

		public WorkerThread(HttpService svc, HttpServerConnection conn)
		{
			super();
			mService = svc;
			mConn = conn;
		}

		public void run()
		{
			Log.i(TAG, "New connection thread");

			HttpContext ctx = new BasicHttpContext(null);

			try {
				while (isInterrupted() == false && mConn.isOpen())
					mService.handleRequest(mConn, ctx);
			} catch (Exception e) {
				Log.e(TAG, "Argh", e);
			} finally {
				if (Thread.interrupted() == false) {
					try {
						mConn.shutdown();
					} catch (IOException e) {}

					synchronized(mWorkers) {
						mWorkers.remove(this);
					}
				}
			}
		}

		public void shutdown()
		{
			interrupt();

			try {
				mConn.shutdown();
			} catch (IOException e) {}
		}

		public void joinUninterruptibly()
		{
			while (true)
			{
				try {
					join();
					break;
				} catch (InterruptedException e) {}
			}
		}
	}
	
	/**
	 * Exactly like the InputStreamEntity except that it "tees" its received
	 * input elsewhere.  That is, it writes both to the HTTP stream and
	 * also to a user-specified location. 
	 */
	public static abstract class TeedInputStreamEntity
	  extends AbstractHttpEntity
	{
		protected InputStream mIn;
		protected long mLength;
		protected OutputStream mOut;
		private boolean mConsumed = false;
		
		public TeedInputStreamEntity() {}

		public TeedInputStreamEntity(InputStream in, OutputStream out,
		  long length)
		{
			super();
			setStream(in, out, length);
		}

		public void setStream(InputStream in, OutputStream out, long length)
		{
			if (in == null)
				throw new IllegalArgumentException();

			if (out == null)
				throw new IllegalArgumentException();

			mIn = in;
			mOut = out;
			mLength = length;			
		}

		public InputStream getContent()
		  throws IOException
		{
			return mIn;
		}

		public long getContentLength()
		{
			return mLength;
		}

		public boolean isRepeatable()
		{
			return false;
		}

		public boolean isStreaming()
		{
			return mConsumed == false;
		}

		public abstract void onConsumed(boolean success);
		public abstract void onWrite(int n);

		public void consumeContent()
		  throws IOException
		{
			Log.d(TAG, "consumeContent()");
			mConsumed = true;
			mIn.close();
			mOut.close();
		}

		public void writeTo(OutputStream out)
		  throws IOException
		{
			if (out == null)
				throw new IllegalArgumentException();

			InputStream in = mIn;
			OutputStream tee = mOut;

			byte[] b = new byte[2048];
			int n;

			try {
				if (mLength < 0)
				{
					while ((n = in.read(b)) >= 0)
					{
						tee.write(b, 0, n);
						out.write(b, 0, n);
						onWrite(n);
					}
				}
				else
				{
					long remaining = mLength;

					while (remaining > 0)
					{
						n = in.read(b, 0, (int)Math.min(b.length, remaining));

						if (n < 0)
							break;

						tee.write(b, 0, n);
						out.write(b, 0, n);
						remaining -= n;
						onWrite(n);
					}
				}

				onConsumed(true);
			} catch (IOException e) {
				onConsumed(false);
			} finally {
				mConsumed = true;
			}
		}
	}
}
